Engineering case study · HealthTech

NutriScan

Photograph a nutrition label and it becomes tracked macros: AI vision reads the calories, protein, carbs, fat and fibre straight off the packet. No barcode database, no manual typing. Around that sits a full tracker: daily goals, a food library, one-tap meal templates, weekly trends, and an AI assistant that already knows your log. It also has a public API and a Claude connector, so a claude.ai chat can read your day and log a meal by description, with every change previewed and confirmed before it saves (connector in private testing).

Live on Vercel · installable PWA · free to try
01 / BY THE NUMBERS
Postgres tables
19
17 under forced RLS
Backend tests
350
run in CI on every push
Free scans / day
10
keeps the AI bill bounded
Label photos stored
0
only the numbers are kept
02 / FROM PHOTO TO MACROS

The scan pipeline

client does the prep, AI does the reading
photo→ crop→ resize + grayscale→ sniff, verify, re-encode→ Gemini vision→ schema-check & normalize→ log

Images are prepared in the browser before they ever leave the phone: a touch-friendly canvas cropper isolates the label, then the image is resized and grayscaled for smaller uploads, faster scans, and cheaper AI calls. On the server every upload passes one validating gate: oversized requests are refused before the body is read, the real format is read from the file's bytes (JPEG, PNG, WebP or iPhone HEIC; the browser's label is ignored), dimensions are checked against a 30-megapixel cap before a pixel is decoded, and the image is re-encoded as a fresh JPEG with no metadata. Nothing else reaches the model, and the photo itself is never stored: only the extracted numbers are. Gemini 2.5 Flash reads the label, with automatic fallback to 3.6 Flash if the primary model fails, and its answer is checked against an explicit schema (allowed fields, sane numeric bounds; text printed on the photo is data, never instructions) and normalized into clean macros (kcal · protein · carbs · fat · fibre) before anything is shown or logged.

Auth without passwords

Supabase · verified server-side
  • ✗No passwords, ever: sign in with Google OAuth or a 6-digit emailed code. Nothing to store, leak, or reset
  • ✓Verified, not trusted: every JWT decode goes through one verifying helper (shared-secret HS256 and cached JWKS) - pinned-ruleset semgrep reports zero findings, and the CI that guards it pins every action and scanner to an immutable hash
  • ✓Isolated at the storage layer: each request binds the user to the database session, and row-level security policies on 17 tables do the rest, through a runtime role that cannot bypass them. App bugs can't read another user's rows
  • ✓Contained when hijacked: a delete spree freezes the account on two layers (in-process lock, Supabase admin ban), revokes every API token, and every deleted row is already in a 30-day recycle bin
  • ✓Deletable, with a way back: deleting starts a 15-day grace period (gated on a sign-in within the last five minutes, the JWT amr claim): the account turns view-and-export only, other devices are signed out, "Keep my account" undoes it, and day 15 erases every row and the login
  • ✓Your data, any time: export everything as an Excel workbook or a zip of CSVs, even during the grace period
03 / ENGINEERING HIGHLIGHTS

F-01Row-level security everywhere

17 of the 19 tables carry forced per-user RLS policies that match nothing when the user is unset, so isolation is enforced by Postgres itself, not by remembering a WHERE clause in every query. Cross-user jobs go through narrow SECURITY DEFINER functions.

F-02Vision with a fallback

Gemini 2.5 Flash is the primary label reader; if it errors, the scan retries on 3.6 Flash automatically, and the app waits out the server's full retry budget instead of re-sending. Users see macros, not model outages.

F-03Context-aware AI assistant

A Groq-powered chat tab that's fed today's log, remaining macros and 7-day averages. Multi-turn, with server-side safety guardrails.

F-04Meal templates

Multi-select foods from the library into a named template, then log an entire breakfast in one tap instead of four.

F-05Real push notifications

VAPID web push with per-user preferences: goal alerts, meal reminders and a weekly summary, each at a time the user picks.

F-06User-controlled PWA updates

New versions don't force-reload mid-log: a banner shows exactly what changed (from a versioned changelog) and the user applies it when ready.

F-07Trends without a chart library

The 7-day trends tab renders its charts as raw SVG: no charting dependency, full control over the visuals, smaller bundle.

F-08Cost-capped by design

A 10-scans-per-day limit per account, plus per-user and per-IP burst caps checked before the paid call, keeps the vision API bill bounded while staying generous for real daily use.

F-09Abuse guards that block, not just alert

One middleware: 300+ requests a minute or a 30-URL 404 walk from one IP earns a 10-minute 429, keyed on the Cloudflare-verified client IP; 20 bad API tokens in five minutes earns the same.

F-10Recycle bin inside Postgres

SECURITY DEFINER BEFORE DELETE triggers on eleven tables copy every deleted row as JSONB into a table the app role cannot read or purge. Retention is hard-coded at 30 days; only the day-15 account purge bypasses it, and then empties that account's rows from the bin too.

F-11Two-layer account freeze

60 verified deletes in ten minutes from one session: every route returns 423 instantly from an in-process lock, and a Supabase admin ban (issued off the event loop) makes it stick across restarts. Reversal is a deliberate runbook step.

F-12Two-channel observability

Web Push to the owner's own devices for 37 hand-written ops events (quota, spikes, budget, keepalive, blocks, freezes) with per-event cooldowns and rolling-window spike detection; Sentry for unhandled exceptions with stack trace and request context.

F-13Uploads that cannot lie

One server-side gate for every image: magic-byte format sniffing (the client's content-type is never trusted), polyglot rejection, a decompression-bomb cap, verify-then-decode, and a metadata-free re-encode. An HTML file named .jpg, a PNG declaring a 30000x30000 canvas, and a photo with "ignore the label" printed on it each have a test; the model's JSON is schema-checked and rejected, never clamped.

F-14A Claude connector that asks first

A Model Context Protocol server on Supabase's OAuth 2.1 server lets claude.ai read the day and log meals. Every write tool saves nothing and returns a preview plus a one-time code; only confirming the code commits, and the code doubles as the idempotency key so a retry can never save twice.

F-15A public REST API done properly

17 endpoints with hashed, scoped personal tokens, per-user rate limits with RateLimit headers, strict request models, idempotency keys, ETag/If-Match on edits, RFC 9457 errors, previews that run the real write and roll back, and an OpenAPI file a test keeps honest.

F-16One Library, one rule

The app and Claude share one food Library. "Ask before saving new foods" is on by default and enforced by the server; it is the only setting Claude may change, and only to off.

F-17Meals with names

Everything Claude logs lands as one named meal marked "via Claude", and any meal can be opened and renamed. The grouping rides inside each row's JSON, so it needed no schema change.

F-18A budget that protects the app

The backend estimates its own free database compute use, alerts at 70% and pauses only the public API at 90%, so the app keeps working for its users. An owner-only admin panel shows it without waking the database.

F-19kcal in storage, your unit on screen

Every write is settled to kcal by one helper (a previewed migration checked all 375 stored values after a backup branch), and each account picks kcal or kJ for display, including in the chat's answers.

04 / SCREENS
NutriScan launch visual: the Scan tab on a phone with the upload-label flow, next to the tagline 'Photograph a label. Get the macros.'
Launch card · photograph a label, get the macros
Crop screen: a nutrition label photo with draggable corner handles isolating the label region
Crop · isolate the label
Scan result: calories, fat, carbs, sugars, fibre, protein and sodium read from the label, with a custom serving calculator
Result · macros read by AI vision
Tracker: daily goal bars for calories, protein, carbs, fat and fibre filled by the logged item
Tracker · logged against daily goals
05 / TRY IT
Get it on your phone

Scan to open NutriScan

Point your phone camera at the code to launch the live app instantly: no install needed, it runs as a PWA you can add to your home screen. Every account gets 10 free scans a day.

nutritional-tracker-delta.vercel.app github.com/MrTig-afk/NutritionalTracker
Scan with camera
06 / BUILT WITH
React 19ViteFastAPI Gemini 2.5 FlashGroqNeon Postgres Supabase Auth + OAuth 2.1Model Context ProtocolWeb Push / VAPID PWA / WorkboxVercelRender